﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;

namespace System.Windows.Forms.Design.Behavior;

/// <summary>
///  This behavior is associated with the ContainerGlyph offered up by ParentControlDesigner.
///  This Behavior simply starts a new DragDrop behavior.
/// </summary>
internal sealed class ContainerSelectorBehavior : Behavior
{
    private Control _containerControl; // our related control
    private IServiceProvider _serviceProvider; // used for starting a drag/drop
    private BehaviorService _behaviorService; // ptr to where we start our drag/drop operation
    private bool _okToMove; // state identifying if we are allowed to move the container
    private Point _initialDragPoint; // cached "mouse down" point

    // For some controls, we want to change the original drag point to be the upper-left of the control
    // in order to make it easier to drop the control at a desired location. But not all controls want this behavior.
    // E.g. we want to do it for Panel and ToolStrip, but not for Label. Label has a ContainerSelectorBehavior via
    // the NoResizeSelectionBorder glyph.
    private readonly bool _setInitialDragPoint;

    /// <summary>
    ///  Constructor, here we cache off all of our member vars and sync location and size changes.
    /// </summary>
    internal ContainerSelectorBehavior(Control containerControl, IServiceProvider serviceProvider)
    {
        Init(containerControl, serviceProvider);
        _setInitialDragPoint = false;
    }

    /// <summary>
    ///  Constructor, here we cache off all of our member vars and sync location and size changes.
    /// </summary>
    internal ContainerSelectorBehavior(Control containerControl, IServiceProvider serviceProvider, bool setInitialDragPoint)
    {
        Init(containerControl, serviceProvider);
        _setInitialDragPoint = setInitialDragPoint;
    }

    [MemberNotNull(nameof(_containerControl))]
    [MemberNotNull(nameof(_serviceProvider))]
    [MemberNotNull(nameof(_behaviorService))]
    private void Init(Control containerControl, IServiceProvider serviceProvider)
    {
        if (!serviceProvider.TryGetService(out BehaviorService? behaviorService))
        {
            Debug.Fail("Could not get the BehaviorService from ContainerSelectorBehavior!");
            return;
        }

        _behaviorService = behaviorService;
        _containerControl = containerControl;
        _serviceProvider = serviceProvider;
        _initialDragPoint = Point.Empty;
        _okToMove = false;
    }

    public Control ContainerControl
    {
        get => _containerControl;
    }

    /// <summary>
    ///  This will be true when we detect a MouseDown on our glyph.
    ///  The Glyph can use this state to always return 'true' from HitTesting
    ///  indicating that it would like all messages (like MouseMove).
    /// </summary>
    public bool OkToMove
    {
        get => _okToMove;
        set => _okToMove = value;
    }

    public Point InitialDragPoint
    {
        get => _initialDragPoint;
        set => _initialDragPoint = value;
    }

    /// <summary>
    ///  If the user selects the ContainerGlyph - select our related component.
    /// </summary>
    public override bool OnMouseDown(Glyph? g, MouseButtons button, Point mouseLoc)
    {
        if (button == MouseButtons.Left)
        {
            // Select our component.
            if (_serviceProvider.TryGetService(out ISelectionService? selSvc)
                && !_containerControl.Equals(selSvc.PrimarySelection as Control))
            {
                selSvc.SetSelectedComponents(new object[] { _containerControl }, SelectionTypes.Primary | SelectionTypes.Toggle);

                // Setting the selected component will create a new glyph, so this instance of the glyph won't receive
                // any more mouse messages. So we need to tell the new glyph what the initialDragPoint and okToMove are.
                if (g is not ContainerSelectorGlyph selOld)
                {
                    return false;
                }

                foreach (Adorner a in _behaviorService.Adorners)
                {
                    foreach (Glyph glyph in a.Glyphs)
                    {
                        if (glyph is not ContainerSelectorGlyph selNew)
                        {
                            continue;
                        }

                        // Don't care if we are looking at the same ContainerSelectorGlyph
                        if (selNew.Equals(selOld))
                        {
                            continue;
                        }

                        // Check if the containerControls are the same
                        if (!(selNew.RelatedBehavior is ContainerSelectorBehavior behNew) || !(selOld.RelatedBehavior is ContainerSelectorBehavior behOld))
                        {
                            continue;
                        }

                        // and the related components are the same, then we have found the new glyph that just got added
                        if (behOld.ContainerControl.Equals(behNew.ContainerControl))
                        {
                            behNew.OkToMove = true;
                            behNew.InitialDragPoint = DetermineInitialDragPoint(mouseLoc);
                            break;
                        }
                    }
                }
            }
            else
            {
                InitialDragPoint = DetermineInitialDragPoint(mouseLoc);
                // set 'okToMove' to true since the user actually clicked down on the glyph
                OkToMove = true;
            }
        }

        return false;
    }

    private Point DetermineInitialDragPoint(Point mouseLoc)
    {
        if (_setInitialDragPoint)
        {
            // Set the mouse location to be to control's location.
            Point controlOrigin = _behaviorService.ControlToAdornerWindow(_containerControl);
            controlOrigin = _behaviorService.AdornerWindowPointToScreen(controlOrigin);
            Cursor.Position = controlOrigin;
            return controlOrigin;
        }
        else
        {
            // This really amounts to doing nothing
            return mouseLoc;
        }
    }

    /// <summary>
    ///  We will compare the mouse loc to the initial point (set in OnMouseDown) and if we're far enough,
    ///  we'll create a <see cref="DropSourceBehavior"/> object and start out drag operation!
    /// </summary>
    public override bool OnMouseMove(Glyph? g, MouseButtons button, Point mouseLoc)
    {
        if (button == MouseButtons.Left && OkToMove)
        {
            if (InitialDragPoint == Point.Empty)
            {
                InitialDragPoint = DetermineInitialDragPoint(mouseLoc);
            }

            Size delta = new(Math.Abs(mouseLoc.X - InitialDragPoint.X), Math.Abs(mouseLoc.Y - InitialDragPoint.Y));
            if (delta.Width >= DesignerUtils.MinDragSize.Width / 2 || delta.Height >= DesignerUtils.MinDragSize.Height / 2)
            {
                // start our drag!
                Point screenLoc = _behaviorService.AdornerWindowToScreen();
                screenLoc.Offset(mouseLoc.X, mouseLoc.Y);
                StartDragOperation(screenLoc);
            }
        }

        return false;
    }

    /// <summary>
    ///  Simply clear the initial drag point, so we can start again on the next mouse down.
    /// </summary>
    public override bool OnMouseUp(Glyph? g, MouseButtons button)
    {
        InitialDragPoint = Point.Empty;
        OkToMove = false;
        return false;
    }

    /// <summary>
    ///  Called when we've identified that we want to start a drag operation with our data container.
    /// </summary>
    private void StartDragOperation(Point initialMouseLocation)
    {
        // Need to grab a hold of some services.
        if (!_serviceProvider.TryGetService(out ISelectionService? selSvc)
            || !_serviceProvider.TryGetService(out IDesignerHost? host))
        {
            Debug.Fail("Can't drag this Container! Either SelectionService is null or DesignerHost is null");
            return;
        }

        // must identify a required parent to avoid dragging mixes of children
        Control? requiredParent = _containerControl.Parent;
        List<IComponent> dragControls = [];
        ICollection selComps = selSvc.GetSelectedComponents();
        // create our list of controls-to-drag
        foreach (IComponent comp in selComps)
        {
            if ((comp is Control ctrl) && (ctrl.Parent is not null))
            {
                if (!ctrl.Parent.Equals(requiredParent))
                {
                    continue; // mixed selection of different parents - don't add this
                }

                if (host.GetDesigner(ctrl) is ControlDesigner des && (des.SelectionRules & SelectionRules.Moveable) != 0)
                {
                    dragControls.Add(ctrl);
                }
            }
        }

        // if we have controls-to-drag, create our new behavior and start the drag/drop operation
        if (dragControls.Count > 0)
        {
            Point controlOrigin;
            if (_setInitialDragPoint)
            {
                // In this case we want the initialMouseLocation to be the control's origin.
                controlOrigin = _behaviorService.ControlToAdornerWindow(_containerControl);
                controlOrigin = _behaviorService.AdornerWindowPointToScreen(controlOrigin);
            }
            else
            {
                controlOrigin = initialMouseLocation;
            }

            DropSourceBehavior dsb = new(dragControls, _containerControl.Parent, controlOrigin);
            try
            {
                _behaviorService.DoDragDrop(dsb);
            }
            finally
            {
                OkToMove = false;
                InitialDragPoint = Point.Empty;
            }
        }
    }
}
